winbrew_engines\windows\exe/
switches.rs

1use anyhow::{Result, bail};
2use std::path::Path;
3
4use crate::models::catalog::package::CatalogInstaller;
5use crate::models::install::installer::InstallerType;
6
7pub(super) fn build_install_args(
8    installer: &CatalogInstaller,
9    install_dir: &Path,
10    package_name: &str,
11) -> Result<Vec<String>> {
12    let mut args = installer
13        .installer_switches
14        .as_deref()
15        .map(split_switches)
16        .transpose()?
17        .unwrap_or_default();
18
19    match installer.kind {
20        InstallerType::Exe => {
21            if args.is_empty() {
22                bail!(
23                    "missing installer switches for generic exe installer '{}'",
24                    package_name
25                );
26            }
27        }
28        InstallerType::Inno => {
29            push_flag_if_missing(&mut args, "/VERYSILENT");
30            push_flag_if_missing(&mut args, "/SUPPRESSMSGBOXES");
31            push_flag_if_missing(&mut args, "/NORESTART");
32            push_flag_if_missing(&mut args, "/SP-");
33
34            if !has_arg_prefix(&args, "/dir=") {
35                args.push(format!(r"/DIR={}", install_dir.display()));
36            }
37        }
38        InstallerType::Nullsoft => {
39            push_flag_if_missing(&mut args, "/S");
40
41            if !has_arg_prefix(&args, "/d=") {
42                args.push(format!(r"/D={}", install_dir.display()));
43            }
44        }
45        InstallerType::Burn => {
46            push_flag_if_missing(&mut args, "/quiet");
47            push_flag_if_missing(&mut args, "/norestart");
48        }
49        _ => {
50            bail!(
51                "native exe backend cannot handle installer kind '{}'",
52                installer.kind.as_str()
53            )
54        }
55    }
56
57    Ok(args)
58}
59
60pub(super) fn split_switches(raw: &str) -> Result<Vec<String>> {
61    let mut args = Vec::new();
62    let mut current = String::new();
63    let mut quote: Option<char> = None;
64
65    for ch in raw.chars() {
66        match ch {
67            '"' | '\'' => match quote {
68                Some(active) if active == ch => {
69                    quote = None;
70                }
71                Some(_) => current.push(ch),
72                None => quote = Some(ch),
73            },
74            ch if ch.is_whitespace() && quote.is_none() => {
75                if !current.is_empty() {
76                    args.push(std::mem::take(&mut current));
77                }
78            }
79            ch => current.push(ch),
80        }
81    }
82
83    if quote.is_some() {
84        bail!("unterminated quoted installer switches: {raw}");
85    }
86
87    if !current.is_empty() {
88        args.push(current);
89    }
90
91    validate_unique_switches(&args, raw)?;
92
93    Ok(args)
94}
95
96fn validate_unique_switches(args: &[String], raw: &str) -> Result<()> {
97    use std::collections::HashSet;
98
99    let mut seen = HashSet::new();
100
101    for arg in args {
102        let signature = switch_signature(arg);
103
104        if !seen.insert(signature) {
105            bail!("duplicate installer switch detected: {arg} in {raw}");
106        }
107    }
108
109    Ok(())
110}
111
112fn switch_signature(arg: &str) -> String {
113    let trimmed = arg.trim();
114
115    match trimmed.split_once('=') {
116        Some((left, _)) => format!("{}=", left.to_ascii_lowercase()),
117        None => trimmed.to_ascii_lowercase(),
118    }
119}
120
121fn push_flag_if_missing(args: &mut Vec<String>, flag: &str) {
122    if !args.iter().any(|arg| arg.eq_ignore_ascii_case(flag)) {
123        args.push(flag.to_string());
124    }
125}
126
127pub(super) fn has_arg_prefix(args: &[String], prefix: &str) -> bool {
128    args.iter()
129        .any(|arg| arg.to_ascii_lowercase().starts_with(prefix))
130}